# 神经网络（深度学习）
"""
一类被称为神经网络的算法最近以“深度学习”的名字再度流行。虽然深度学习在许多机
器学习应用中都有巨大的潜力，但深度学习算法往往经过精确调整，只适用于特定的使
用场景。这里只讨论一些相对简单的方法，即用于分类和回归的多层感知机（multilayer
perceptron，MLP），它可以作为研究更复杂的深度学习方法的起点。MLP 也被称为（普
通）前馈神经网络，有时也简称为神经网络。
"""
import mglearn.plots
from IPython.core.display_functions import display
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split

# 神经网络模型
'''
MLP 可以被视为广义的线性模型，执行多层处理后得到结论。
还记得线性回归的预测公式为：
ŷ = w[0] * x[0] + w[1] * x[1] + … + w[p] * x[p] + b
简单来说，ŷ 是输入特征 x[0] 到 x[p] 的加权求和，权重为学到的系数 w[0] 到 w[p]。我们可
以将这个公式可视化，
'''
display(mglearn.plots.plot_logistic_regression_graph())

'''
在 MLP 中，多次重复这个计算加权求和的过程，首先计算代表中间过程的隐单元（hidden unit），然后再计算这些隐单元的加权求和并得到最终结果
'''
display(mglearn.plots.plot_single_hidden_layer_graph())

'''
这个模型需要学习更多的系数（也叫作权重）：在每个输入与每个隐单元（隐单元组成了隐层）之间有一个系数，在每个隐单元与输出之间也有一个系数。
'''

'''
这个模型需要学习多个权重：每个输入与隐单元之间有一个权重，每个隐单元与输出之间也有一个权重。
为了使模型比线性模型更强大，我们需要在每个隐单元的加权求和后应用非线性函数，如 ReLU 或 tanh。
ReLU 会将小于 0 的值设为 0，而 tanh 将值映射到 -1 到 1 之间。这些非线性函数使神经网络能够学习更复杂的函数
'''

# 可以添加多个隐层，这些由许多计算层组成的大型神经网络，正是术语“深度学习”的灵感来源。
mglearn.plots.plot_two_hidden_layer_graph()

# 神经网络调参
# 我们将 MLPClassifier 应用到本章前面用过的 two_moons 数据集上，以此研究 MLP 的工作原理。

from sklearn.neural_network import MLPClassifier
from sklearn.datasets import make_moons, load_breast_cancer

X, y = make_moons(n_samples=100, noise=0.25, random_state=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,
 random_state=42)
mlp = MLPClassifier(solver='lbfgs', random_state=0).fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
# plt.show()

#  如你所见，神经网络学到的决策边界完全是非线性的，但相对平滑。我们用到了
# solver='lbfgs'，这一点稍后会讲到。

'''
默认情况下，MLP 使用 100 个隐结点，这对于这个小型数据集来说已经相当多了。我们可
以减少其数量（从而降低了模型复杂度），但仍然得到很好的结果
'''

# 要设置最大迭代数
mlp = MLPClassifier(solver='lbfgs', random_state=0, hidden_layer_sizes=[10], max_iter=1000)
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
# plt.show()

'''
当只有 10 个隐单元时，决策边界会显得不平滑。默认使用 ReLU 激活函数。
如果只有一个隐层，决策边界由 10 个直线段组成。要获得更平滑的边界，
可以增加隐单元数量、增加第二个隐层，或者使用 tanh 激活函数。
'''

# 使用2个隐层，每个包含10个单元
mlp = MLPClassifier(solver='lbfgs', random_state=0,
 hidden_layer_sizes=[10, 10], max_iter=1000)
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")

# 使用2个隐层，每个包含10个单元，这次使用tanh非线性
mlp = MLPClassifier(solver='lbfgs', activation='tanh',
 random_state=0, hidden_layer_sizes=[10, 10], max_iter=1000)
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
# plt.show()

'''
我们可以通过 L2 惩罚使权重趋向于 0，从而控制神经网络的复杂度，类似于在岭回归和线性分类器中的做法。
在 MLPClassifier 中，L2 惩罚的参数是 alpha，默认值很小，表示弱正则化。
下面展示了不同 alpha 值对 two_moons 数据集的影响，使用的是 2 个隐层的神经网络，每层有 10 个或 100 个单元。
'''
fig, axes = plt.subplots(2, 4, figsize=(20, 8))
for axx, n_hidden_nodes in zip(axes, [10, 100]):
     for ax, alpha in zip(axx, [0.0001, 0.01, 0.1, 1]):
         mlp = MLPClassifier(solver='lbfgs', random_state=0,
         hidden_layer_sizes=[n_hidden_nodes, n_hidden_nodes],max_iter=1000,
         alpha=alpha)
         mlp.fit(X_train, y_train)
         mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
         mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)
         ax.set_title("n_hidden=[{}, {}]\nalpha={:.4f}".format(
         n_hidden_nodes, n_hidden_nodes, alpha))

# plt.show()

'''
控制神经网络复杂度的方法有很多，包括隐层的数量、每个隐层的单元数以及正则化参数（如 alpha）。实际上还有其他方法，但这里不再详细讨论
'''

'''
神经网络在开始学习之前，其权重是随机初始化的，这种初始化会影响最终学到的模型。
即使使用相同的参数，不同的随机种子可能导致不同的模型结果。
对于大型且复杂度合适的网络，这种影响通常不大，但对于较小的网络，这一点需要特别注意
'''

# 图 2-53 显示了几个模型的图像，所有模型都使用相同的参数设置进行学习：
# 每次的random_state不同
fig,axes = plt.subplots(2, 4, figsize=(20, 8))
for i,ax in enumerate(axes.ravel()):
    mlp = MLPClassifier(solver='lbfgs', random_state=i,hidden_layer_sizes=[100, 100],max_iter=1000)
    mlp.fit(X_train, y_train)
    mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3,ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train,ax=ax)
# plt.show()

# 为了在现实世界的数据上进一步理解神经网络，我们将 MLPClassifier 应用在乳腺癌数据
# 集上。首先使用默认参数：
cancer = load_breast_cancer()
print("Cancer data per-feature maxima:\n{}".format(cancer.data.max(axis=0)))

X_train, X_test, y_train, y_test = train_test_split(
 cancer.data, cancer.target, random_state=0)
mlp = MLPClassifier(random_state=42)
mlp.fit(X_train, y_train)
print("Accuracy on training set: {:.2f}".format(mlp.score(X_train, y_train)))
print("Accuracy on test set: {:.2f}".format(mlp.score(X_test, y_test)))

# Accuracy on training set: 0.94
# Accuracy on test set: 0.92

'''
MLP 的精度不错，但不如其他模型。这可能是因为数据没有进行适当的缩放，就像之前的 SVC 示例一样。
神经网络需要输入特征的变化范围相似，理想情况下是均值为 0、方差为 1。因此，我们需要对数据进行缩放以满足这些要求。
虽然这里我们手动完成，但在第 3 章中会介绍如何使用 StandardScaler 自动进行缩放
'''

# 计算训练集中每个特征的平均值
mean_on_train = X_train.mean(axis=0)
# 计算训练集中每个特征的标准差
std_on_train = X_train.std(axis=0)
# 减去平均值，然后乘以标准差的倒数
# 如此运算之后，mean=0，std=1
X_train_scaled = (X_train - mean_on_train) / std_on_train
# 对测试集做相同的变换（使用训练集的平均值和标准差）
X_test_scaled = (X_test - mean_on_train) / std_on_train
mlp = MLPClassifier(random_state=0)
mlp.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
 mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))
# Accuracy on training set: 0.991
# Accuracy on test set: 0.965

'''
增加迭代次数只提升了训练集的性能，但没有改善模型的泛化能力。尽管模型的表现还可以，但训练集和测试集之间的性能差距表明，
我们可以通过降低模型复杂度来提高泛化性能。为此，我们可以增大 alpha 参数，从而对权重施加更强的正则化：
'''

mlp = MLPClassifier(max_iter=1000, alpha=1, random_state=0)
mlp.fit(X_train_scaled, y_train)
print("Accuracy on training set: {:.3f}".format(
 mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))

'''
不过，关于如何分析神经网络学到的内容，通常比线性模型或基于树的模型要复杂。一种方法是查看模型的权重。
例如，在 scikit-learn 的示例库中，有一个关于如何可视化神经网络在 MNIST 数据集上学到的特征的示例。
这个示例可以帮助理解神经网络是如何通过权重来捕捉数据中的模式和特征的。
'''

'''
 优点
- 强大的数据处理能力：能够从大量数据中提取信息，构建复杂的模型。
- 广泛的适用性：在分类和回归任务中通常能超越其他机器学习算法。
- 灵活性：可以通过调整参数来适应不同的数据和任务需求.

 缺点
- 训练时间长：特别是对于大型神经网络，训练过程可能需要很长时间.
- 数据预处理要求高：需要仔细地预处理数据，以确保所有特征具有相似的含义.
- 调参复杂：调节神经网络模型和训练模型的方法多种多样，需要一定的经验和技巧.

 参数
- 层数和每层的隐单元个数：决定了模型的复杂度和学习能力。通常从 1 个或 2 个隐层开始，逐步增加。每个隐层的结点个数通常与输入特征个数接近.
- 正则化参数 alpha：用于控制权重的大小，防止过拟合。增大 alpha 可以增强正则化，提高泛化性能.
- 学习算法（solver）：
  - 'adam'：默认选项，适用于大多数情况，但对数据缩放敏感，因此需要将数据缩放为均值为 0、方差为 1.
  - 'lbfgs'：鲁棒性好，但在大型模型或大型数据集上训练时间较长.
  - 'sgd'：更高级的选项，常用于深度学习研究，但需要调节多个参数以获得最佳结果.

 调参建议
- 初始网络设置：创建一个足够大的网络以确保能够过拟合，从而验证网络可以学习任务.
- 调整策略：在确认训练数据可以被学习后，可以通过缩小网络规模或增大 alpha 来提高泛化性能.
- 学习算法选择：对于初学者，建议使用 'adam' 和 'lbfgs'，根据实际情况选择合适的算法.

'''

'''
fit 会重置模型
scikit-learn 模型的一个重要性质就是，调用 fit 总会重置模型之前学到的
所有内容。因此，如果你在一个数据集上构建模型，然后在另一个数据集上再次
调用 fit，那么模型会“忘记”从第一个数据集中学到的所有内容。你可以
对一个模型多次调用 fit，其结果与在“新”模型上调用 fit 是完全相同的。
'''

